Self-hosted Calendly alternatives compared
Cal.com, Easy!Appointments, Rallly and DayOtter on your own VPS, compared on the two things that decide it: two-way calendar sync and outbound email.
The short answer
A self-hosted Calendly alternative has to do one thing the internal tools on your VPS never do: answer to the public. The booking page is the product. It needs a real domain name and TLS (transport layer security) on day one, and it has to deliver mail to people who have never heard of your server.
Four projects cover the realistic field. Cal.com is the closest match to Calendly and the default pick for a solo consultant. Easy!Appointments is the light one, PHP and MySQL, happy on a 1 GB VPS. Rallly is a group poll tool and has no booking page at all. DayOtter is the newest entrant, an AGPLv3 scheduling platform with a confirm-first assistant in front of it.
Two questions decide which one you can actually run. Does it sync both ways with the calendar you already live in? And can it send mail? The second question is where most self-hosted booking setups quietly fail, so it goes first.
Outbound email is the part that fails
A booking confirmation goes to a stranger's inbox. That is transactional mail landing at Gmail or Microsoft 365, and those receivers judge you on the sending IP address and on your DNS records.
Sending straight from the VPS almost never works. Most providers block outbound TCP port 25 on new accounts, so the connection hangs and then times out. Even where port 25 is open, a fresh VPS address has no sending history, and large receivers treat unknown hosting-range addresses as suspect. The booking is written to the database, the page says confirmed, and nobody gets an email. Nothing looks broken from the server side, which is why this is usually found weeks later by a client who never showed up.
Use a relay. Any transactional mail provider works, and the app only ever needs a hostname, a port, a user and a password. Check the port is reachable before you touch application config:
nc -vz -w 5 "$SMTP_HOST" 587A succeeded line means the path is open. A hang or Connection refused means the port is blocked at the network level, and no amount of editing .env will fix that. Relays listen on 587 or 465 precisely because 25 is so often blocked.
Each project takes the relay in its own way. Cal.com reads EMAIL_FROM, EMAIL_SERVER_HOST, EMAIL_SERVER_PORT, EMAIL_SERVER_USER and EMAIL_SERVER_PASSWORD, and also accepts a RESEND_API_KEY instead. Watch that one: the shipped .env.example points EMAIL_SERVER_HOST at localhost on port 1025, which is a local development mailbox. Leave the default in place and the app sends into nothing without an error. Rallly takes SMTP_HOST, SMTP_PORT, SMTP_USER and SMTP_PWD. DayOtter takes SMTP settings or a Resend key. Easy!Appointments sends its notifications from the application, so point it at the same relay from its settings page before you accept a real booking.
Then publish the DNS records your relay gives you. An SPF (sender policy framework) record says which servers may send for your domain, and a DKIM (domainkeys identified mail) key signs each message so the receiver can prove it was not altered. Add a DMARC (domain-based message authentication, reporting and conformance) policy once both pass. Send a test booking to a real address at a large provider, open the message headers, and confirm the authentication lines read pass. A booking page that cannot send is worse than no booking page, because it fails silently.
Which calendar backends really sync both ways
Sync has two directions and they fail separately. The read direction is availability: the app must see your existing busy blocks or it will hand out a slot you are already in. The write direction is the booking itself: the confirmed event has to appear on the calendar you actually look at, not only inside the booking tool.
Google Calendar and Microsoft 365 do both directions, with one condition on a self-hosted install. You create the OAuth (open authorization) client yourself, because the hosted product's client ID is not in the source code. For Cal.com that is GOOGLE_API_CREDENTIALS in .env, holding the JSON you download from the Google Cloud console. DayOtter takes Google and Microsoft OAuth credentials the same way.
Two things break here, and both are worth knowing before you start. First, the redirect URI you register must match your public URL exactly, including the scheme and any trailing path, or Google stops the connection with redirect_uri_mismatch at the consent screen. Second, a Google project left in Testing publishing status issues refresh tokens that expire after seven days. Sync works all week and then stops, and the app logs show invalid_grant on the next refresh. Move the consent screen to In production, or accept reconnecting by hand every Monday.
CalDAV (calendaring extensions to WebDAV) is the open option, and support is thinner. Cal.com ships a CalDAV app that is still marked beta, verified against servers including Baikal, Radicale, Nextcloud and Kerio Connect. Apple iCloud works through the same app but needs an app-specific password rather than your Apple ID password. DayOtter lists Apple via CalDAV next to Google and Microsoft 365.
An ICS feed is not a sync. A subscribed .ics URL is read-only by design, so it can block time on your booking page but it can never receive the booking. If a tool offers only ICS for your calendar, you have half the plumbing and you will still be copying events by hand.
Easy!Appointments syncs Google Calendar and nothing else. Rallly does not read availability at all: it collects votes on a set of candidate dates. That is the right tool for "when can the six of us meet" and the wrong tool for "book 30 minutes with me".
A booking page is public, so TLS comes first
Most of what people self-host is private. A wiki, a board, a dashboard, all of it can sit behind a VPN or an SSO login and never see the open internet. A booking link cannot. Anyone you send it to has to load it, which changes the setup in three concrete ways.
You need a domain name with an A record pointed at the VPS, before you install anything. You need a certificate on day one, because browsers mark a plain HTTP form as not secure and your client is typing their name and email into it. And you need the app's public URL set correctly in its config, because that value is baked into the links inside outgoing email and into the OAuth redirect URIs. Set NEXT_PUBLIC_WEBAPP_URL in Cal.com, DOMAIN in Rallly, BASE_URL in Easy!Appointments, or DAYOTTER_DOMAIN at install time, and set it to the https:// address you will really use.
Rallly and DayOtter solve TLS for you. Rallly's bundled stack includes Traefik and issues Let's Encrypt certificates using the address in ACME_EMAIL. DayOtter's installer brings up Caddy with automatic HTTPS. Cal.com and Easy!Appointments do not, so you put nginx in front and issue the certificate yourself, the same way you would for a Let's Encrypt certificate on nginx with Certbot. Bind the app container to 127.0.0.1 so the only way in is through the proxy you control. If the same box already runs a self-hosted Trello alternative for your internal boards, keep that behind your existing auth and give only the booking host a public server block.
Cal.com on your VPS
The Docker configuration lives in its own repository, and the images are prebuilt on Docker Hub, so you pull rather than build.
git clone --recursive https://github.com/calcom/cal.diy.git
cd cal.diy
cp .env.example .env
openssl rand -base64 32
openssl rand -base64 24
docker compose pull
docker compose up -dThe first random value goes in NEXTAUTH_SECRET and the second in CALENDSO_ENCRYPTION_KEY. Both are required. Set DATABASE_URL and point NEXT_PUBLIC_WEBAPP_URL at your public address. The bundled stack is the web app, PostgreSQL and Prisma Studio; the docs give docker compose up -d calcom for running the app alone against a database you host elsewhere, which is what you want once the install is settled.
Pull the image, do not build it on the VPS. The project's own instructions tell you to export NODE_OPTIONS="--max-old-space-size=16384" when building from source, which is a 16 GB heap for Node alone. On ARM hardware, add the -arm suffix to the image tag. The project publishes no minimum for running the prebuilt image, so treat 2 GB for the app plus PostgreSQL as my working figure rather than a documented one, and watch memory during the first week.
Check it came up:
docker compose ps
docker compose logs -f calcom
curl -sI https://cal.example.com | head -n 1The curl should print HTTP/2 200. A 502 Bad Gateway from nginx while the container shows as running usually means the first boot is still applying database migrations. Give it a few minutes and read the logs before you conclude it is broken. Cal.com's webhooks fire on every confirmed booking, so a booking can drive whatever automation you already run, for example an n8n instance reachable over HTTPS on your VPS.
The core is AGPLv3, with some features held in an enterprise directory under a separate commercial licence. Read that licence before you build a paid business process on the team features.
Easy!Appointments on a 1 GB box
Requirements are Apache or Nginx, PHP 8.2 or newer, and MySQL. There is an official image at alextselegidis/easyappointments.
One warning first. The docker-compose.yml in the repository is a development environment. It expects you to open a shell in the container and run npm install && composer install && npm start. It is not a deployment. Use the published image instead:
services:
easyappointments:
image: alextselegidis/easyappointments # pin the current tag from Docker Hub
environment:
- BASE_URL=https://book.example.com
- DB_HOST=mysql
- DB_NAME=easyappointments
- DB_USERNAME=easyapp
- DB_PASSWORD=change-me
ports:
- '127.0.0.1:8080:80'
depends_on:
- mysql
mysql:
image: mysql:8.0
environment:
- MYSQL_ROOT_PASSWORD=change-me-too
- MYSQL_DATABASE=easyappointments
- MYSQL_USER=easyapp
- MYSQL_PASSWORD=change-me
volumes:
- ./mysql:/var/lib/mysqlBASE_URL must be the public HTTPS address. Get it wrong and the booking links inside confirmation emails point at a host your client cannot reach. The image serves plain HTTP on port 80 with no certificate of its own, which is why the port is bound to 127.0.0.1 and nginx terminates TLS in front. If the compose syntax is new to you, start with Docker Compose basics on a VPS and come back.
This is the lightest option here by a wide margin. Two containers, a PHP application and MySQL, sit comfortably on a 1 GB VPS. The cost is reach: Google Calendar is the only calendar backend, and the interface is a traditional admin panel rather than a modern booking flow. If your calendar is Microsoft 365, Fastmail or Nextcloud, this one is out before you start.
Rallly for group polls
Rallly answers a different question. It does not publish your availability. It puts a set of candidate times in front of a group and collects votes, which is what you want for a board meeting and useless for a client booking link.
curl -fsSL https://get.rallly.co | bashRead any script before you pipe it into a shell. Swap bash for less, read what it does, then run it. The manual path does the same work in steps you can see:
git clone https://github.com/lukevella/rallly-selfhosted.git
cd rallly-selfhosted
./rallly.sh setup
./rallly.sh startThe documented requirements are at least 2 GB of RAM, Docker 19.03 or newer with Compose v2, ports 80 and 443 free, and a domain pointed at the server. The bundled stack is Traefik for HTTPS, the web application, PostgreSQL, and Garage for S3-compatible object storage. Set DOMAIN, a SECRET_PASSWORD of at least 32 characters, SUPPORT_EMAIL and INITIAL_ADMIN_EMAIL. If you already run a reverse proxy, set PROXY_MODE=external and WEB_PORT, and Traefik stays out of the way. If you already run a self-hosted S3-compatible object store with MinIO, point the S3_* variables at it and drop the Garage container.
SMTP is not optional here, because sign-in is a magic link. With no working relay nobody can log in, including the admin account you just created. That is the good version of the email failure: it stops you at the door instead of losing a client's booking three weeks later.
DayOtter, the newest entrant
DayOtter is an AGPLv3 scheduling platform with an assistant attached. The production install is one command:
curl -fsSL https://raw.githubusercontent.com/Dayotter/dayotter/main/deploy/install.sh \
| sudo DAYOTTER_DOMAIN=cal.example.com bashRead it before you run it, as above. The installer sets up Docker, generates secrets, and brings up the full stack: the Next.js web app, a background worker that handles reminders and calendar sync and webhooks, PostgreSQL, Redis, and Caddy with automatic HTTPS.
Calendar support is the broadest of the four. Google, Microsoft 365, Apple through CalDAV, and ICS feeds, with the ICS caveat above. Every other integration is opt-in through environment variables, including SMTP or Resend for mail, ANTHROPIC_API_KEY for the assistant, Twilio for SMS, and Stripe for payments. The assistant is confirm-first: it proposes, you approve, and nothing reaches your calendar without an explicit yes. Leave the API key empty and that part of the product simply does not run.
Licensing is clean for self-hosters. The core is AGPLv3, and an ee/ directory carries a commercial cloud-only licence that stays inert unless DAYOTTER_CLOUD=1 is set. That means the team features the hosted plan bills at $9 per seat each month, as of August 2026, are available on your own server.
This is also the heaviest stack here and the youngest project. Run it next to your existing booking link for two weeks, take real bookings through both, and read the worker logs before you move your clients over.
What each stack actually costs you
The container count is the honest proxy for how much a stack will ask of a small VPS, since each service carries its own memory floor. These are the counts from each project's own published Docker stack, read in August 2026.
The data behind this chart
[
{
"tool": "Easy!Appointments",
"containers": 2,
"database": "MySQL"
},
{
"tool": "Cal.com",
"containers": 3,
"database": "PostgreSQL"
},
{
"tool": "Rallly",
"containers": 4,
"database": "PostgreSQL"
},
{
"tool": "DayOtter",
"containers": 5,
"database": "PostgreSQL and Redis"
}
]Easy!Appointments needs 2 containers and fits on 1 GB. Rallly's bundled stack is 4, and its documentation asks for 2 GB. DayOtter's installer brings up 5, which is why it wants the largest box of the 4 here. Cal.com and DayOtter publish no minimum memory figure at all, so 2 GB is my starting point for both rather than a supported number.
Two of these counts drop if you already run infrastructure. Rallly's Traefik and Garage containers both come out when you point it at your own proxy and object storage. Cal.com's Prisma Studio is a development tool you should not leave running on a public server.
Which self-hosted Calendly alternative should you pick
A solo consultant should run Cal.com. It is the only project here that combines a booking page people recognise, prebuilt images that spare your VPS a Node build, and a CalDAV path for those of us who do not keep a calendar at Google or Microsoft. One PostgreSQL database and one application container is a maintenance load you can carry for years. Budget an afternoon for the OAuth client and the mail relay, and note that the CalDAV app is still beta, so test one real booking end to end before you publish the link.
A small team should look at DayOtter. Weighted round robin and collective booking sit in the AGPLv3 core, so self-hosting gives you the features a hosted seat charges for, and the worker process is built for the reminders and webhooks a team actually leans on. The trade is maturity: it is the newest project on this list, so run it in parallel first and keep the old link alive until you have watched a full month of bookings.
Two narrower cases. If all you need is a poll to find a time the group can meet, install Rallly and stop there. If you have a 1 GB VPS, you live in Google Calendar, and you want the smallest thing that takes a booking, Easy!Appointments will outlive every fancier option you could put on that box. For the wider question of what else earns space on the same server, see what is worth self-hosting in 2026.
FAQ
Can I run a self-hosted booking page without a domain name?
No. Every one of these apps writes its public URL into the links inside confirmation emails, and Google and Microsoft both match the OAuth redirect URI against that same value, so a bare IP address gives you redirect_uri_mismatch at the consent screen. Let's Encrypt will not issue a certificate for an IP address either, so the page loads over plain HTTP and the browser marks the form as not secure. Buy the domain first, point an A record at the VPS, then install.
Why do my booking confirmation emails never arrive?
Almost always because the server is trying to deliver the mail itself. Most VPS providers block outbound port 25 on new accounts, so the connection hangs, and even where it is open a new address has no sending reputation and large receivers refuse it. Point the app at a transactional mail relay on port 587, confirm the port is reachable with nc -vz -w 5 "$SMTP_HOST" 587, then publish the SPF and DKIM records the relay gives you. If you are running Cal.com, check that you replaced the shipped EMAIL_SERVER_HOST=localhost and EMAIL_SERVER_PORT=1025 defaults, which point at a local development mailbox.
Does self-hosted Cal.com sync with CalDAV, or only Google?
Both, with different maturity. The CalDAV app is marked beta and is verified against servers including Baikal, Radicale, Nextcloud and Kerio Connect, and Apple iCloud works through it using an app-specific password. Google Calendar and Microsoft 365 both sync in each direction, but on a self-hosted install you must create your own OAuth client and supply it through GOOGLE_API_CREDENTIALS, because the hosted service's credentials are not in the source.
Why does my Google Calendar sync stop working after a week?
Because the Google Cloud project is still in Testing publishing status. Google issues refresh tokens to apps in that state which expire after seven days, so the connection works, then dies on the next token refresh, and the application log shows invalid_grant. Move the OAuth consent screen to In production and reconnect the calendar once. Reconnecting without changing the status buys you another seven days and no more.
Which of these will run on a 1 GB VPS?
Easy!Appointments will, since it is a PHP application plus MySQL. Rallly documents a 2 GB minimum and its bundled stack runs four services. Cal.com and DayOtter publish no minimum, but a Next.js application with PostgreSQL, and in DayOtter's case Redis and a worker process as well, means you should plan on 2 GB or more. Never build Cal.com from source on a small box: the project's own build instructions ask for a 16 GB Node heap, so pull the prebuilt image instead.