How to Self-Host a Shared Calendar with Docker
Learn how to self-host Musubi with Docker Compose, HTTPS, PostgreSQL backups, updates and calendar sync on infrastructure you control.

Using a hosted calendar is convenient.
Someone else maintains the server, installs updates, monitors the database and makes sure the application is available when you need it.
Self-hosting changes that arrangement.
You run the calendar server on infrastructure you control. You decide where the database lives, when the software is updated, how backups are stored and who has administrative access.
That control comes with responsibility.
This guide explains how to self-host Musubi using Docker Compose, connect the mobile app, add HTTPS, create backups and keep the installation updated.
You do not need to compile the application or install Node.js. The standard setup uses a prebuilt Musubi API image together with PostgreSQL.
What you will be running
The default Musubi self-hosting stack contains two services:
- the Musubi API server
- a PostgreSQL database
Docker Compose defines both services, creates the private network between them and stores the database in a persistent Docker volume. The API waits for PostgreSQL to become healthy before starting and runs database migrations automatically.
The mobile application connects directly to your API server.
You can then create users and shared calendars, invite members and optionally connect external providers such as:
- Google Calendar
- Apple and iCloud
- Outlook and Microsoft 365
- Nextcloud
- Radicale
- Fastmail
- other CalDAV servers
Google, Microsoft and CalDAV integrations require additional credentials. The basic Musubi server can start without them.

What self-hosting changes
Self-hosting gives you control over the server running Musubi.
You choose:
- the hosting provider
- the physical region
- the domain
- the backup destination
- the update schedule
- the logging and monitoring setup
- which authentication providers are enabled
- who has access to the operating system and database
Self-hosting does not mean the calendar suddenly requires no trust.
You still need to trust:
- the operating system
- Docker
- your hosting provider
- your reverse proxy
- connected calendar providers
- anyone with server administration access
- your own backup and security practices
Connecting Google Calendar or Outlook also means communicating with those providers. Running Musubi yourself does not turn externally synchronized calendars into isolated local data.
Self-hosting gives you more control over the Musubi part of the system. It does not remove every external dependency.
Should you self-host your calendar?
Self-hosting may be a good fit when:
- you already operate a home server or VPS
- you want to choose where the database is stored
- you are comfortable maintaining Docker services
- you want control over backups and retention
- you want to modify the application
- you run services for a household, organisation or community
- operating the infrastructure is part of the value for you
The hosted version may be a better fit when:
- you do not want to manage a public server
- you do not want to monitor security updates
- you have no backup and recovery process
- you need someone else to be responsible for availability
- you only want to install the app and start planning
Neither choice is more legitimate.
Open source means you have the option to self-host. It does not mean everyone should become a server administrator.
What you need before starting
For a basic installation, you need:
- a server with Docker Engine
- the Docker Compose plugin
- enough storage for PostgreSQL and backups
- a domain name if the server will be used outside your local network
- access to your DNS configuration
- a reverse proxy for HTTPS
Musubi’s official requirements list Docker and Docker Compose, with a domain recommended for HTTPS. Docker currently recommends Docker Desktop or the Compose plugin; the old standalone docker-compose installation is maintained mainly for backward compatibility.
This guide uses the current command format:
docker compose
rather than the older:
docker-compose
1. Check that Docker is available
Connect to your server over SSH and verify that Docker and Compose are installed:
docker --version docker compose version
Both commands should return version information.
On a new Linux server, install Docker Engine and the Compose plugin using the official instructions for your distribution. Docker recommends installing the plugin through its package repository so it can be updated with the rest of the Docker installation.
You can also confirm that the Docker daemon is running:
docker info
Depending on how Docker was installed, your user may need sudo permission to run Docker commands.
2. Choose a domain
You can run Musubi locally using an IP address and port.
For regular use across the internet, a domain with HTTPS is strongly recommended.
A typical address might be:
musubi.example.com
Create a DNS record pointing that hostname to your server’s public IP address.
Later, the same address will be used for:
- the public API
- authentication callbacks
- password-reset links
- account-deletion confirmation links
- the custom server address entered in the mobile application
Musubi uses the BETTER_AUTH_URL environment variable as its public origin, so the configured value needs to match the address users actually open.
3. Download the Docker Compose configuration
Create an empty directory for the installation:
mkdir -p ~/musubi cd ~/musubi
Download the Compose file and environment template from the official repository:
curl -O https://raw.githubusercontent.com/frgtn-dot-dev/musubi/main/docker-compose.yml curl -o .env https://raw.githubusercontent.com/frgtn-dot-dev/musubi/main/.env.example
The standard Compose configuration pulls the prebuilt frgtndev/musubi API image and starts PostgreSQL 17 alongside it. You therefore do not need to clone the complete repository or build the application for a normal installation.
Your directory should now contain:
musubi/ ├── .env └── docker-compose.yml
Before continuing, read the downloaded files.
Self-hosting instructions often encourage copying commands quickly. Configuration files deserve inspection, especially when they start network services and databases.
4. Configure the environment
Open the .env file with your preferred editor:
nano .env
At minimum, configure the database credentials, authentication secret, public URL and production environment.
A simplified example looks like this:
POSTGRES_USER=musubi POSTGRES_PASSWORD=replace-with-a-long-random-password POSTGRES_DB=musubi BETTER_AUTH_SECRET=replace-with-a-different-long-random-secret BETTER_AUTH_URL=https://musubi.example.com ENVIRONMENT=production API_SERVER_PORT=7531 LOG_LEVEL=info
Do not reuse the same value for the database password and the authentication secret.
Generate the authentication secret
Musubi recommends generating BETTER_AUTH_SECRET with OpenSSL:
openssl rand -base64 32
Copy the generated value into .env.
Keep this secret stable. Changing it invalidates existing sessions and signs users out.
Generate a database password
You can generate another random value for PostgreSQL:
openssl rand -base64 36
If the result contains characters that cause problems in an environment file or connection string, generate a hexadecimal value instead:
openssl rand -hex 32
Keep the .env file outside public repositories and restrict its permissions:
chmod 600 .env
5. Decide which optional features to enable
Musubi starts without Google, Microsoft, SMTP or CalDAV credentials. The corresponding features remain unavailable until you configure them.
You do not need to configure every integration on the first day.
Start with email and password authentication, confirm that the installation works, and then add external providers one at a time.
Google Calendar
Google authentication and Google Calendar synchronization require:
GOOGLE_WEB_CLIENT_ID= GOOGLE_CLIENT_SECRET=
You will need to create an OAuth application and configure its authorised origins and callback addresses according to the Musubi authentication and Google provider documentation.
Without these values, the mobile app hides the Google sign-in option rather than showing a broken button. The client asks the server which providers are enabled when it opens.
Outlook and Microsoft 365
Microsoft synchronization requires an application registration and these values:
MICROSOFT_CLIENT_ID= MICROSOFT_CLIENT_SECRET= MICROSOFT_TENANT_ID=common
The web redirect URI for the Microsoft application needs to use this pattern:
https://musubi.example.com/api/auth/callback/microsoft
The tenant defaults to common when another value is not supplied.
Apple and iCloud
Musubi connects iCloud calendars through CalDAV.
Stored CalDAV passwords are encrypted using:
CALDAV_ENC_KEY=
Configure this key before allowing anyone to connect a CalDAV account. Musubi identifies it as required for storing CalDAV credentials securely.
Use a separate random value:
openssl rand -hex 32
Keep this key backed up securely. Losing encryption keys can make stored credentials unusable even when the database itself has been restored.
Native Sign in with Apple uses additional Apple Developer configuration. It is separate from connecting an iCloud calendar through CalDAV.
Email delivery
Configure SMTP when email-and-password users need password resets or in-app account-deletion confirmation:
SMTP_HOST= SMTP_PORT= SMTP_USER= SMTP_PASS= FROM_EMAIL=
Without SMTP, the application can still run, but those email-dependent flows are unavailable. The client adapts by hiding the forgotten-password option and informing users that deletion must be handled by the administrator. Social-login users do not rely on a Musubi password-reset email.
For a private test instance, SMTP can wait.
For a real multi-user installation, configure it before inviting people.
6. Validate the Compose configuration
Before starting containers, ask Docker Compose to render and validate the complete configuration:
docker compose config
This catches many issues involving:
- missing required variables
- malformed YAML
- incorrect environment syntax
- unresolved substitutions
The rendered output may contain sensitive configuration. Do not paste it into a public issue without reviewing and removing secrets.
You can also list the images the stack will use:
docker compose config --images
The normal setup should include the Musubi API image and PostgreSQL.
7. Start Musubi
Start the stack in detached mode:
docker compose up -d
Compose will:
- pull the required images
- create an internal network
- create the persistent database volume
- start PostgreSQL
- wait for its health check
- start the Musubi API
- apply database migrations
Musubi publishes the API on API_SERVER_PORT, which defaults to port 7531.
Check the service status:
docker compose ps
Both the api and db services should be running. PostgreSQL should report a healthy status.
View the API logs:
docker compose logs -f api
Press Ctrl+C to stop following the logs. This does not stop the container.
Musubi writes structured JSON logs to standard output and includes a request identifier in responses and related log entries. According to the current documentation, request bodies, passwords and tokens are excluded from those logs.
8. Test the server locally
Before configuring the domain, test the API from the server itself:
curl http://127.0.0.1:7531/api/v1/server
Change the port if you modified API_SERVER_PORT.
A response from this endpoint confirms that:
- the API process is reachable
- the server has started
- the reverse-proxy target will be available
- the client can discover enabled authentication features
Do not treat one successful request as complete production monitoring. It is only the first health check.
9. Put HTTPS in front of Musubi
For internet-facing use, place a reverse proxy such as Caddy, nginx or Traefik in front of the published API port.
The proxy receives HTTPS traffic and forwards it to Musubi over the local connection. Musubi’s documentation explicitly recommends this arrangement and requires BETTER_AUTH_URL to use the public HTTPS origin.
Example with Caddy
A minimal Caddy configuration can look like this:
musubi.example.com {
reverse_proxy 127.0.0.1:7531
}
Replace the domain and port with your own values.
Caddy’s documented reverse_proxy directive supports forwarding all requests for a domain to a local backend in this form.
Once the proxy is active, test the public endpoint:
curl https://musubi.example.com/api/v1/server
Confirm that .env contains the same origin:
BETTER_AUTH_URL=https://musubi.example.com
After changing .env, recreate the API container:
docker compose up -d
Compose detects configuration changes and recreates affected containers while preserving mounted volumes.
Avoid exposing PostgreSQL publicly
The default Musubi Compose file does not publish the PostgreSQL port to the host. The API reaches it over the private Compose network.
Keep it that way unless you have a specific administrative requirement and understand how to restrict database access.
A calendar database should not be listening openly on the internet.
10. Connect the mobile application
When Musubi opens for the first time, the welcome screen allows users to enter a custom server address.
Enter:
https://musubi.example.com
The app requests the server-information endpoint and displays only the authentication methods configured on that instance. If Google authentication is not configured, for example, the Google button does not appear.
Test the complete flow:
- Create an account
- Sign out
- Sign in again
- Create a calendar
- Create an event
- Invite another test user
- Edit the event from the second device
- Restart the API and confirm that the data remains
Do this before inviting your family or relying on the installation for important plans.
11. Understand where the data lives
The only stateful component in the standard stack is PostgreSQL.
Its files are stored in a named Docker volume mounted at:
/var/lib/postgresql/data
inside the database container.
Stopping or replacing a container does not normally remove the named volume.
However, this command is dangerous:
docker compose down -v
The -v option removes named volumes declared by the Compose project. Docker documents that behaviour explicitly.
For ordinary restarts, use:
docker compose restart
or:
docker compose down docker compose up -d
Do not add -v unless your intention is to remove the stored data.
A Docker volume is persistent storage. It is not a backup.
If the server disk fails, the volume fails with it.
12. Create a database backup
Musubi includes a PostgreSQL backup script for plain Docker Compose installations.
When using the full repository, run:
./ops/backup-postgres.sh
The script creates a validated database dump and removes old local backups according to the configured retention period. The current defaults use a local backups directory and retain 14 days.
To store backups elsewhere and retain them for 30 days:
MUSUBI_BACKUP_DIR=/var/backups/musubi \ MUSUBI_BACKUP_RETENTION_DAYS=30 \ ./ops/backup-postgres.sh
If you installed only docker-compose.yml and .env, download or copy the backup scripts from the official repository before using them.
Most importantly, move the resulting backups off the Musubi server.
Possible destinations include:
- another physical machine
- encrypted object storage
- an encrypted backup server
- an offline drive
- a storage account in a separate provider
A backup stored only on the same server does not protect you against disk failure, deletion of the virtual machine or loss of the hosting account. Musubi’s documentation recommends keeping at least one encrypted copy outside the server.
PostgreSQL custom-format dumps are designed to work with pg_restore and are generally more portable than copying raw database files between installations.

13. Test the backup
A backup that has never been restored is an assumption.
Musubi includes a recovery-verification script:
./ops/verify-postgres-backup.sh /path/to/backup.dump
The script starts a temporary PostgreSQL container, restores the supplied dump, checks that public tables exist and then removes the temporary container. It does not connect to the production database. Both the local script’s dump format and the compressed format produced by the documented Dokploy setup are supported.
Run a recovery test:
- after creating the backup process
- after changing database credentials
- after changing the backup destination
- after major PostgreSQL changes
- periodically even when nothing appears to have changed
For a real recovery, stop the API, restore into a clean database, run current migrations, start the API and verify login plus at least one calendar read before reopening the server to users. Keep the old database untouched until validation succeeds.
14. Update the installation
To update a prebuilt-image installation:
cd ~/musubi docker compose pull docker compose up -d
The API runs current database migrations when it starts.
Then check:
docker compose ps docker compose logs --tail=100 api
Before updating:
- Read the release notes or changelog
- Create a database backup
- Verify that the backup exists outside the server
- Record the currently running image tag
- Plan a short maintenance window for important instances
Musubi remains pre-1.0 and its documentation warns that breaking changes are possible during active development.
Pin a version
By default, the Compose file uses the latest tag.
You can select a specific published version in .env:
MUSUBI_TAG=1.2.0
Replace the example with a tag that actually exists in the image registry.
Pinning gives you control over when a new release is introduced. It does not eliminate the need to follow security fixes.
15. Monitor the service
For a small household installation, useful monitoring can begin with:
- checking whether the public API responds
- alerting when the server is unreachable
- monitoring free disk space
- monitoring backup completion
- reviewing container restarts
- checking certificate renewal
- periodically testing login and event creation
Musubi also exposes Prometheus metrics on a separate internal listener. The default metrics port is 9464, and setting METRICS_PORT=0 disables it. The standard Compose file does not publish the metrics port to the host.
The available metrics cover process health, HTTP requests, latency, active requests and external calendar synchronization failures. The repository also contains example alert rules for API availability, repeated server errors and provider-sync failures.
Do not expose the metrics endpoint publicly.
Place Prometheus on the relevant private Docker network or restrict access through your network configuration.
16. Run only one API instance
Musubi currently supports one API process for each database.
Scheduling, cleanup tasks, live Server-Sent Events connections and rate limiting include process-local state. The API also obtains a PostgreSQL advisory lock and exits when another API process is already using the same database.
Do not increase the Compose replica count to create horizontal scaling.
For a normal household or small-group server, one API process is the intended configuration.
Availability should instead come from:
- reliable infrastructure
- monitoring
- restart policies
- database backups
- a documented recovery procedure
17. Build Musubi from source
The prebuilt image is the easiest route.
To inspect, modify or build the server yourself, clone the repository:
git clone https://github.com/frgtn-dot-dev/musubi.git cd musubi
In docker-compose.yml, comment out the prebuilt image: entry and enable the included build: section:
build: context: . dockerfile: apps/api/Dockerfile
Then build and start the stack:
docker compose up -d --build
The current repository places the API in apps/api, while shared authentication, calendar logic, database code and types live in separate packages.
Building from source is useful when:
- testing a pull request
- maintaining a private patch
- developing a provider adapter
- auditing the exact build
- contributing upstream
For ordinary operation, the published image avoids the build toolchain and shortens deployment.
A practical production checklist
Before inviting real users, confirm that:
- the public address uses HTTPS
BETTER_AUTH_URLexactly matches that address- database and authentication secrets are random and different
.envis not committed to Git- PostgreSQL is not publicly exposed
- SMTP works when email/password accounts are offered
CALDAV_ENC_KEYis configured before CalDAV accounts are connected- backups run automatically
- backups leave the Musubi server
- at least one restoration test has succeeded
- disk usage is monitored
- server and container updates are planned
- only one API replica uses the database
- users know who operates the instance
- users know how to contact the administrator
Self-hosting is not completed when the login screen appears.
It is completed when you know how to update, monitor, back up and recover the service.
Hosted or self-hosted?
The hosted and self-hosted versions solve different operational needs.
Use the hosted service when you want:
- the fastest setup
- no server maintenance
- managed infrastructure
- managed updates
- fewer operational responsibilities
Self-host when you want:
- control over the server location
- control over backups
- control over software updates
- private modifications
- integration with existing infrastructure
- the ability to operate independently
Both use the same open-source foundation.
The difference is who operates it.
With the hosted service, that responsibility belongs to the Musubi team.
With self-hosting, it belongs to you.
A shared calendar on your own server
A calendar is quiet infrastructure.
When it works, people rarely think about the database, authentication service, synchronization jobs or TLS certificate behind it.
They simply know where they need to be.
Docker Compose makes the initial Musubi deployment relatively small: one API, one PostgreSQL database and one persistent volume. The application image is prebuilt, migrations run automatically and the mobile app supports a custom server address.
That makes installation approachable.
The serious part starts afterwards.
Protect the secrets. Use HTTPS. Configure email. Keep the server updated. Store backups somewhere else. Restore one before trusting it.
The goal is not merely to run your own calendar.
It is to run one your people can rely on.
Try Musubi
Musubi is an open-source, self-hostable shared calendar for partners, households, families and small groups.
Run the official hosted version or deploy the API and PostgreSQL stack on your own infrastructure using Docker Compose.
Read the self-hosting documentation, learn what open source gives you, or explore the source code on GitHub.
Frequently asked questions
Can I self-host Musubi with Docker?
Yes. Musubi provides a Docker Compose file that starts the API server and PostgreSQL database. The normal installation pulls a prebuilt API image, so cloning or compiling the source is not required.
Does Musubi include PostgreSQL?
The Compose configuration starts a PostgreSQL 17 container and stores its data in a named Docker volume. You do not need a separate PostgreSQL installation for the default setup.
Do I need a domain?
A domain is not necessary for local testing, but it is recommended for a server used over the internet. A domain makes it practical to configure HTTPS and provides a stable address for authentication and the mobile application.
Does Musubi require HTTPS?
The API can technically be reached over HTTP in a trusted local environment. For normal remote use, put a reverse proxy in front of it and use an HTTPS `BETTER_AUTH_URL`. Musubi’s self-hosting documentation recommends Caddy, nginx or Traefik for this.
Can I use Google Calendar with a self-hosted instance?
Yes. You need to create and configure Google OAuth credentials for your own server. If those credentials are absent, the server still runs but Google sign-in and Google Calendar synchronization remain unavailable.
Can I connect iCloud to a self-hosted Musubi server?
Yes. Apple and iCloud calendar synchronization uses CalDAV. Configure `CALDAV_ENC_KEY` before allowing users to store CalDAV credentials.
Can I run several Musubi API replicas?
Not against the same database at present. Musubi currently enforces one API process per database using a PostgreSQL advisory lock.
How do I update Musubi?
For the prebuilt-image installation, run: docker compose pull docker compose up -d Database migrations execute during API startup. Create and verify a backup before applying an important update.
How do I back up a self-hosted Musubi installation?
Back up the PostgreSQL database. Musubi includes a script for creating and validating dumps and another script for testing that a dump can be restored. Keep an encrypted backup outside the server.
Does a Docker volume count as a backup?
No. A Docker volume keeps database data outside the writable layer of a container, but it normally remains on the same server. It does not protect against loss of that server or disk.
Is self-hosted Musubi end-to-end encrypted?
Self-hosting gives you control over the server, but the server still needs to process calendar events for sharing and external synchronization. Self-hosting should not be confused with end-to-end encryption.
Can I build Musubi from source?
Yes. Clone the MIT-licensed repository and enable the `build:` section included in the Compose file instead of using the published image.