Skip to contentLaunch price30% off every kit for the first 250 buyers155 left
DirectoryLab

Loading the guides…

Local LabGuides
Go live

Deploy to a server

Put one Local Lab site on a server of your own with the kit's deploy script, from a bare machine to the setup wizard.

Updated 7 Oct 2026 · written for Local Lab 1.2.1

On this page

The kit's own script, scripts/deploy.sh, puts Local Lab on a server you rent. It installs Docker, a PostgreSQL database and the nginx web server, then runs each site in a container of its own. This guide takes one site from a bare server to the setup wizard. Every command on the server is run as root.

What you need#

  • A server (VPS) running Debian or Ubuntu. The script uses apt-get, systemctl and nginx's sites-enabled folder, so it does not work on anything else. It caps each site at 768 MB of memory, and its own notes reckon that three sites and the database fit in 4 GB.

  • Root access over SSH. There is no sudo in the script.

  • A domain name whose DNS records you can edit.

  • Ports 80 and 443 open to the internet. The script does not set up a firewall.

  • A few small tools the script uses and does not install:

On the server
apt-get update && apt-get install -y curl openssl python3 unzip git

Deploy the site#

The examples use a site called lawn and the domain example.com. Put your own in their place.

  1. Step 1: Put the code in /opt/directorylab

    With repository access, clone it. The server needs permission to read the repository, and later updates pull the main branch from origin.

    With repository access
    git clone YOUR-REPOSITORY-ADDRESS /opt/directorylab

    With the zip, copy it up from your own computer and unpack it.

    On your computer
    scp local-lab-1.2.1.zip root@YOUR-SERVER-IP:/opt/
    On the server
    cd /opt
    unzip local-lab-1.2.1.zip
    mv local-lab-1.2.1 directorylab
  2. Step 2: Run first-run

    bash
    cd /opt/directorylab
    ./scripts/deploy.sh first-run

    Wait until it tells you to add a site. What it sets up is listed below.

  3. Step 3: Set your domain

    Every new site is given the address <name>.<BASE_DOMAIN>. Until you set it, BASE_DOMAIN is directorylab.io, a domain that is not yours, so set it before you add a site.

    bash
    export BASE_DOMAIN=example.com
    echo 'BASE_DOMAIN=example.com' >> /etc/environment

    The first line is for this session. The second is for every later one. Then, in your DNS, add an A record for lawn.example.com that points at the server's IP address.

  4. Step 4: Add the site

    bash
    ./scripts/deploy.sh add-site lawn

    Use lower-case letters and digits only in the name. It becomes part of the database name and the first part of the address. The script does not check it, and a hyphen, a dot, an underscore or a capital letter breaks one or the other.

  5. Step 5: Open the site and finish the wizard

    Go to https://lawn.example.com. Your browser warns that the certificate is not trusted, because add-site made a temporary one. Go past the warning this once and complete the wizard, as in Install Local Lab.

    The Local Lab setup wizard with the site name Greenkeep, the industry Lawn Care, the country United States and the admin account fields filled in
    The setup wizard, shown once on a new install.
  6. Step 6: Set the website address

    Sign in at https://lawn.example.com/admin. Open Site Settings and, on the General tab, fill in Website Address (Base URL) with https://lawn.example.com. Links in emails are built from it. Left empty, they point at https://example.com.

  7. Step 7: Switch on backups and get a real certificate

    bash
    bash scripts/setup-crons.sh

    first-run and add-site do not install the nightly backup. This does: see Backups, scheduled jobs and PostgreSQL. For the certificate, and for a domain of your own in place of lawn.example.com, see More sites, domains and HTTPS.

What first-run sets up#

  • Docker and its Compose plugin, if they are missing.

  • nginx, if it is missing. Its default site is removed.

  • certbot, for Let's Encrypt certificates.

  • Folders: sites/ and backups/ inside /opt/directorylab, and /etc/ssl/directorylab for certificates.

  • A PostgreSQL password, made at random and saved in sites/.pgpass.

  • PostgreSQL 16, in a container called directorylab-postgres. It answers only on the server itself, and its data is kept in a Docker volume.

  • The Local Lab image, called directorylab, built from the Dockerfile. Every site on the server runs from it.

You can run it again. It skips what is already there and rebuilds the image.

What add-site creates#

WhatWhere and how
A foldersites/lawn/, holding .env, .port and uploads/.
A databasedirectorylab_lawn, in the PostgreSQL container, with every table made.
A settings filesites/lawn/.env, with a new SECRET_KEY and the database address.
A containerdirectorylab-lawn. Docker restarts it if it stops. It runs two workers of four threads each and may use 768 MB.
A port5001 for the first site, 5002 for the next. It can be reached only from the server itself: nginx passes visitors to it.
An nginx file/etc/nginx/sites-enabled/directorylab-lawn.conf, which sends lawn.example.com to the site and turns http into https.
A certificateA temporary, self-signed one at /etc/ssl/directorylab/lawn.pem and lawn.key.

Pictures that you and business owners upload are kept in sites/lawn/uploads/, outside the container, so they outlive restarts and updates.

The settings file#

add-site has already written what a live site needs into sites/lawn/.env. Open it with nano sites/lawn/.env.

  • SECRET_KEY: a random key made for this site. Leave it alone. A live site will not start without one: the container stops with "SECRET_KEY is not set (or is the insecure default) in a production environment" and Docker starts it again, over and over. Once the site has saved keys, never change it.

  • DATABASE_URL, FLASK_ENV=production, PREFERRED_URL_SCHEME=https and the three lines about workers, threads and the pool: leave them.

  • Email, Stripe and GOOGLE_PLACES_API_KEY: written empty. Enter these in the admin and leave the file alone: see Connect Stripe and Email, campaigns and bounces. What you save in the admin is used ahead of this file.

  • GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, RECAPTCHA_SITE_KEY and RECAPTCHA_SECRET_KEY: these can only be set here. Left empty, Google sign-in and reCAPTCHA are off. See Telegram, Google sign-in and reCAPTCHA.

Write each line as KEY=value, with no quotes and no spaces round the equals sign. A change does nothing until you restart the site:

bash
./scripts/deploy.sh update-site lawn

update-site stops the container and starts a new one that reads the file again. The site does not answer in between.

Check on it#

bash
./scripts/deploy.sh status

This prints a table with a row for each site: SITE, PORT, DOMAIN and STATUS.

  • UP with 200 or 302 means the site answered. A new site answers 302 until the wizard is done.

  • WARN with any other number means the container is running and the site is not answering properly.

  • DOWN means the container is not running.

Under the table are the size of each database, and the processor and memory each container is using. The DOMAIN column always shows <name>.<BASE_DOMAIN>, even after you give the site a domain of its own.

bash
./scripts/deploy.sh logs lawn

This shows the last 100 lines the site has written and keeps following it. Press Ctrl and C to stop. For more, add a number: ./scripts/deploy.sh logs lawn 500.

add-site says the site already exists

An earlier attempt stopped part of the way through and left its folder behind. Clear it and start again:

bash
./scripts/deploy.sh remove-site lawn

Answer y to both questions, then run add-site again.

The browser shows 502 Bad Gateway

nginx is running and the site's container is not answering. Run ./scripts/deploy.sh status, then ./scripts/deploy.sh logs lawn to see why it stopped.

Do I have to use lawn.example.com?

No. That is only the first address. Add your own domain once the site is up.

How do I change the code on the server later?

See Updating Local Lab. With repository access it is one command. With a zip it is a few.

Stuck on a step? Send a message.