🗄️

A private server for several websites on Docker and nginx

our own product: site isolation, HTTPS, self-hosted mail and backups

In production · in-house product
1 entry
one nginx serves HTTP and HTTPS for all sites, and a bare IP address returns nothing
1 site, 1 network
every site in its own Docker network, and sites cannot see each other
2 weeks
of nightly mail snapshots, including the mail-signing key, are kept

The task

Until 18 September 2026 the studio's websites were scattered across the VPN machines: the business card, the greeting-card site and a demo site on the Russian relay, client staging sites on the second Russian node, and the greeting-card service in Moldova. Every site edit risked taking the VPN down for all clients. The relay has 1.9 GB of memory and had already gone down once because of a neighbouring service. Certificates could not be issued for the second node, so one of the sites ran without HTTPS.

The studio needed a separate server where sites share neither memory, nor nginx, nor certificates with the VPN, and where each site is protected from its neighbours.

The solution

A dedicated studio web server in Moscow, whose configuration is kept in the lab-server repository. What is done:

  • One entry nginx accepts HTTP and HTTPS, terminates TLS and routes requests to the sites.
  • Each site runs in its own container and its own Docker network, edge-<site>; sites cannot see one another.
  • All sites open over HTTPS and HTTP is redirected. A bare IP address returns nothing: the handshake is rejected and the connection is closed.
  • Let's Encrypt certificates renew themselves, and nginx reloads its configuration after renewal.
  • The entry nginx writes its logs to a file with rotation, and Docker logs are size-capped. The server and containers run in the Europe/Moscow time zone.
  • Self-hosted mail for the studio's domain: receiving, sending and a web interface.
  • Nightly snapshots of the mail and of the data of hosted sites, a watchdog for the VPN chain and a job that pulls a copy of the VPN database.
  • Tunnels to services abroad, so the sites can reach bots and APIs without a direct connection.

How it works

One entry nginx for all sites

nginx is a web server that here acts as the single point of entry. It takes a request, looks at the site name and hands the request to the right site's container. The entry nginx configuration lives in a directory mounted into the container as a whole, so an edit is applied with a reload and the container does not have to be recreated. Unknown names are rejected.

Site isolation with Docker networks

A Docker network is a virtual network inside which containers see only each other. Every site gets its own edge-<site> network, and only the site's container and the entry nginx are on it. Site ports are not published to the outside. Databases are attached only to the internal networks of their own projects, so a break-in or failure in one site gives no path to a neighbour's database. A new site is added by one pattern: a network, a container, a name in the configuration, a certificate and a site file in conf.d.

HTTPS certificates: auto-renewal and nginx reload

Certificates are issued by certbot, with ownership checked through a webroot directory, so nginx never has to stop. Renewal is driven by the certbot.timer timer. A separate hook after each renewal makes nginx re-read the certificate: without it, nginx would keep serving the old one until it expired. A check script walks through all the sites and shows each certificate's remaining lifetime.

Self-hosted mail on the domain: docker-mailserver

A single mailserver container holds Postfix (sending and receiving), Dovecot (IMAP access), Rspamd (spam filtering) and fail2ban. The web interface is Roundcube. Antivirus is switched off: the ClamAV signature database takes more than a gigabyte out of four. Spam is not discarded but placed in the "Junk" folder, because at a small volume losing a client's message is worse than sorting a folder by hand. Mail to a non-existent address is rejected. A separate container forwards every new message to a service Telegram chat: subject, sender, text and attachments as files; spam arrives as a single line without a sound.

Tunnels to services abroad

Some services, including the Telegram bots, stay abroad, and from Russia new TCP connections to them often do not get through the filter. So connections are opened in advance and from abroad: ten independent frp clients in one balancing group carry the sites' requests to the service. If one session drops, new connections go to the other nine. The studio's lead bot (site requests into Telegram) reaches Telegram and an AI API through a separate egress tunnel.

Results

  • Sites and the VPN are separated: editing a site can no longer take down the VPN.
  • Every site is isolated in its own Docker network, and a bare IP address returns no content.
  • Certificates renew automatically, and nginx picks them up without a restart.
  • Domain mail is received on the studio's own server, and messages also arrive in Telegram.
  • Nightly mail snapshots are kept for two weeks.

Technologies and why

  • nginx — the entry point: TLS, request routing by site, logs.
  • Docker Compose — a container and a Docker network for each site.
  • certbot (Let's Encrypt) — free certificates with auto-renewal through webroot.
  • docker-mailserver, Roundcube — self-hosted domain mail and its web interface.
  • frp — tunnels through which the sites reach services abroad.
  • Python — the studio's lead bot and the service that forwards mail to Telegram.
  • logrotate, cron — log rotation and nightly snapshots.

Status

Version 1.16.0, dated 29 September 2026. The server has been in production since 18 September 2026, when the sites moved off the VPN servers. Since 29 September 2026 it has also run the studio's main site on Nuxt, alongside the business-card site of engineer Valery Afanasyev. Because the server lacks the memory to build Nuxt, the image is built on a workstation and loaded with docker load.

Open items per the README: outgoing mail is blocked by the hosting provider and queues up; some WordPress demo sites have no scheduled backups; the server sits with the same provider as the VPN nodes, so the sites are not decoupled from provider problems; the Telegram bots remain abroad and are reached through tunnels.

Questions about this project

How do I host several websites on one server so they do not interfere with each other?
One entry nginx receives every request, terminates TLS and routes it to the right site. Each site runs in its own container and its own Docker network, so sites cannot see one another. Databases are attached only to the internal networks of their own projects, and the entry nginx cannot reach them.
Why a separate server for websites when VPN servers already exist?
Sites used to live on the VPN machines, and every site edit risked taking down the VPN for all clients: the relay, with 1.9 GB of memory, once went down because of a neighbouring service. A separate server breaks that link: sites and the VPN no longer share memory, nginx or certificates.
How are HTTPS certificates renewed without manual work or downtime?
Let's Encrypt certificates are renewed by certbot on a timer. After each renewal a hook tells the entry nginx, which reloads its configuration without restarting the container. A check script shows the remaining lifetime of every certificate; the alert threshold is under 20 days.
Can I run mail on my own domain instead of the hosting provider's?
Yes. docker-mailserver, with Postfix, Dovecot, Rspamd and fail2ban, handles receiving and sending, and mail can be read in the Roundcube web interface or any mail client. DNS records are required: the server address, MX, SPF, DKIM, DMARC and a reverse PTR record for the IP address.
Are backups kept?
Nightly snapshots cover the mail (kept for two weeks) and the database and files of some hosted sites; a separate script pulls a copy of the VPN database. Some WordPress demo sites still have no scheduled backups, which is listed under limitations.
What does not work as well as we would like yet?
The hosting provider blocks outgoing connections on mail ports: receiving mail works, while outgoing messages queue in Postfix and will go out by themselves once a way out appears. The workarounds are a plan change, sending through an external relay over an HTTPS API, or a tunnel to a server abroad.

Need something similar?

Tell us about the task — we'll show how we solved it and estimate the scope.