Self-hosting stack for a small VPS: what fits in 2 GB and 4 GB RAM
A realistic self-hosted starter stack on a small VPS: Docker Compose, Caddy, Vaultwarden, Uptime Kuma and Forgejo, a RAM budget per service, backups and security basics.
Short answer: a 2 GB VPS comfortably runs a self-hosted core of Caddy (HTTPS reverse proxy), Vaultwarden (passwords), Uptime Kuma (monitoring) and Forgejo (git) under Docker Compose, using roughly 0.6 to 1 GB of RAM. Add Nextcloud and you want 4 GB; choose lighter tools such as Syncthing or File Browser and 2 GB is still enough.
This guide gives a working Compose file, a RAM budget per service, and the two things most selfhosted setups skip until it is too late: off-site backups and a locked-down network surface. Commands assume Debian 12/13 or Ubuntu 24.04 with Docker Engine and the Compose plugin installed from Docker's official repository.
RAM budget per service
Figures are typical idle-to-light-use resident memory for one to five users with SQLite where supported. Peaks during upgrades, git clones of large repositories or Nextcloud previews can be higher, so leave at least 20% headroom and add a 1 to 2 GB swap file.
| Service | Role | Typical RAM | Notes |
|---|---|---|---|
| OS + sshd + journald | Base system | 150–250 MB | Debian or Ubuntu minimal |
| Docker Engine + containerd | Container runtime | 80–120 MB | Fixed cost of using containers |
| Caddy | Reverse proxy, automatic HTTPS | 20–50 MB | Single binary, no database |
| Vaultwarden | Bitwarden-compatible password server | 20–60 MB | Written in Rust, SQLite |
| Uptime Kuma | Uptime and certificate monitoring | 100–200 MB | Node.js; grows with monitor count |
| Forgejo | Git hosting, issues, CI hooks | 150–300 MB | Go, SQLite fine for small teams |
| Nextcloud + MariaDB + Redis | Files, calendar, contacts | 600 MB–1.2 GB | PHP-FPM workers dominate |
| Syncthing | File sync, no web suite | 50–150 MB | Lighter Nextcloud alternative |
| File Browser | Web file manager | 20–40 MB | Upload and share files over HTTPS |
| Radicale | CalDAV and CardDAV | 30–50 MB | Calendar and contacts without Nextcloud |
Choosing the VPS size
Core stack (Caddy, Vaultwarden, Uptime Kuma, Forgejo) plus base system lands around 0.6 to 1 GB, which fits a 2 vCPU / 2 GB server with room for Syncthing and Radicale. Nextcloud with MariaDB and Redis pushes the total to about 1.5 to 2.2 GB, so 4 GB is the realistic minimum if you want the full suite plus everything else. Disk matters too: Forgejo repositories and synced files grow, while the services themselves need only a few GB.
On NoctisNode the Pulse plan (2 vCPU, 2 GB RAM, 40 GB NVMe, $10 per 28 days) fits the core stack, and Umbra (4 vCPU, 4 GB, 60 GB NVMe, $15 per 28 days) fits the core plus Nextcloud. Both are KVM with full root and a dedicated IPv4, and can be paid in Monero or other crypto. Nothing in this guide depends on the provider.
Docker Compose file
Only Caddy publishes web ports; the apps are reachable solely through the internal Compose network. Forgejo also publishes port 2222 for git over SSH, so your host OpenSSH stays on its own port. Set `FORGEJO_TAG` in a `.env` file to the current Forgejo major version and replace the example domains with your own DNS records.
# /srv/stack/compose.yaml
services:
caddy:
image: caddy:2
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy/data:/data
- ./caddy/config:/config
vaultwarden:
image: vaultwarden/server:latest
restart: unless-stopped
environment:
DOMAIN: "https://vault.example.com"
SIGNUPS_ALLOWED: "true" # set to "false" after creating your account
volumes:
- ./vaultwarden:/data
uptime-kuma:
image: louislam/uptime-kuma:2
restart: unless-stopped
volumes:
- ./uptime-kuma:/app/data
forgejo:
image: codeberg.org/forgejo/forgejo:${FORGEJO_TAG}
restart: unless-stopped
environment:
USER_UID: "1000"
USER_GID: "1000"
FORGEJO__service__DISABLE_REGISTRATION: "true"
volumes:
- ./forgejo:/data
ports:
- "2222:22"
# /srv/stack/Caddyfile
# vault.example.com {
# reverse_proxy vaultwarden:80
# }
# status.example.com {
# reverse_proxy uptime-kuma:3001
# }
# git.example.com {
# reverse_proxy forgejo:3000
# }Bringing it up
- 01.Point A (and AAAA) records for `vault`, `status` and `git` at the server's IP, so Caddy can obtain certificates on first start.
- 02.Create `/srv/stack`, place `compose.yaml`, `Caddyfile` and `.env` there, and run `docker compose up -d`.
- 03.Open the Forgejo URL and finish the installer; the first account you create through it becomes the admin, since public registration is disabled.
- 04.Create your Vaultwarden account, then set `SIGNUPS_ALLOWED` to `"false"` and run `docker compose up -d` again. Leave the admin panel disabled unless you set an Argon2-hashed `ADMIN_TOKEN`.
- 05.Create the Uptime Kuma admin user immediately after start, because the setup page is open to anyone until you do.
- 06.Add monitors for each service and for certificate expiry, ideally from a second location, since a monitor on the same server cannot report its own outage.
Note · Caddy handles HTTPS automatically. For headers, rate limits and more complex routing, see the Caddy reverse proxy guide.
Backups that actually restore
Everything worth keeping lives under `/srv/stack`: SQLite databases, Forgejo repositories, Caddy certificates and your config. Do not count on provider snapshots; keep your own encrypted, off-site copy with restic, as described in the restic backups guide.
SQLite files copied while a service is writing can be inconsistent. The simplest safe approach on a small server is to stop the stack for the minute a backup takes. If you add Nextcloud with MariaDB, dump the database with `mariadb-dump` before the restic run. Test a restore to a scratch directory at least once; a backup you have never restored is a guess.
#!/bin/sh
# /usr/local/sbin/stack-backup (run nightly from cron or a systemd timer)
# needs RESTIC_REPOSITORY and RESTIC_PASSWORD_FILE in the environment
cd /srv/stack || exit 1
docker compose stop
rc=0
restic backup /srv/stack --tag stack || rc=$?
docker compose start
[ "$rc" -eq 0 ] || exit "$rc"
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --pruneSecurity basics
- ▹SSH: keys only, no root password login, optionally a non-standard port; see [the SSH hardening guide](/guides/ssh-hardening).
- ▹Firewall caveat: ports published by Docker bypass `ufw` rules, because Docker writes its own iptables/nftables chains. Publish only what must be public (80, 443, 2222) and never publish database or admin ports.
- ▹Updates: enable `unattended-upgrades` for the host, and run `docker compose pull && docker compose up -d` on a schedule you actually keep.
- ▹Registration: keep public sign-ups off in Vaultwarden and Forgejo; invite users instead.
- ▹Disk encryption: put `/srv` on an [encrypted root](/guides/luks-encrypted-root) so a copied disk image reveals nothing; on a remote server use [LUKS remote unlock over SSH](/guides/luks-remote-unlock-ssh), and prefer the LUKS2 format explained in [LUKS2 vs LUKS1](/guides/luks2-vs-luks1).
- ▹Secrets: `.env` and the Vaultwarden data directory should be readable by root only (`chmod 600`/`700`).
FAQ
QHow much RAM do I need for self-hosting?
AA core self-hosted stack of Caddy, Vaultwarden, Uptime Kuma and Forgejo runs in about 0.6 to 1 GB including the OS and Docker, so a 2 GB VPS is enough. Adding Nextcloud with MariaDB and Redis makes 4 GB the practical minimum.
QCan I run Nextcloud on a 2 GB VPS?
AIt runs, but alongside other services it leaves little headroom and becomes slow under previews or sync bursts. On 2 GB, Syncthing plus File Browser and Radicale cover files, calendar and contacts with a fraction of the memory.
QIs Vaultwarden safe to self-host?
AVaultwarden stores only end-to-end encrypted vault data, so a server compromise does not expose plaintext passwords directly. Keep sign-ups off, keep the admin panel disabled or protected by a hashed token, update regularly and back up its data directory.
QWhat is the best reverse proxy for a selfhosted Docker stack?
ACaddy is the easiest for small setups because it obtains and renews HTTPS certificates automatically with a few lines of config. Nginx and Traefik also work but need more configuration.
QDoes ufw protect Docker containers?
ANot by default. Ports published with Docker's `ports:` option bypass ufw because Docker inserts its own firewall rules, so publish only the ports that should be public.