Deployment
This chapter explains in detail how to put Vallotton on a server. Once it is deployed, you can open Vallotton from anywhere (including your phone) through your own domain name, and the painting happens on the server too, so your own computer does not have to stay on. The chapter assumes you can log in to a Linux server and work on its command line. If you only want to use Vallotton on your own computer, the quick start is all you need.
What you need
- A Linux server reachable from the internet. The commands below use Ubuntu as the example. The painting is done on NovelAI's side, so the server does not need a graphics card.
- A domain name. At your domain registrar, add an A record pointing to the server's IP. Vallotton can only be logged in to through an HTTPS domain; if you open it by IP address, you will not be able to log in.
- Your NovelAI and DeepSeek keys, the same ones the quick start asks for. See two accounts for how to get them.
- Docker, nginx and certbot installed on the server. Docker runs Vallotton, nginx connects your domain to Vallotton, and certbot gets you a free HTTPS certificate:
curl -fsSL https://get.docker.com | sh sudo apt install nginx certbot python3-certbot-nginx - Ports 22, 80 and 443 opened in your cloud provider's console (usually under “security groups” or “firewall”). 22 is for logging in to the server, 80 and 443 for the web page.
Deployment steps
Once deployed, Vallotton runs inside Docker on the server and opens a single port on the server itself only (8300 by default). When your domain is visited from outside, the request first reaches nginx on the server, which handles HTTPS, and is then passed on to Vallotton. The steps are explained in order below.
1. Download the code
Log in to the server and download the code to a fixed location, for example /srv/vallotton/app. Also create a directory just for the finished pictures, for example /srv/vallotton/media. The rest of this chapter uses these two paths as the example; if you choose different ones, adjust the commands below to match.
sudo mkdir -p /srv/vallotton/media
sudo chown -R $USER /srv/vallotton
git clone https://github.com/FFTYYY/ProjectVallotton.git /srv/vallotton/app
cd /srv/vallotton/app
All the commands below assume you are in this code directory, /srv/vallotton/app.
2. Fill in .env
All of Vallotton's configuration, including every key and password, lives in the .env file in the code directory. Start by copying the template:
cp .env.example .env
Then open it in an editor (for example nano .env) and change these items:
| Item | What to enter |
|---|---|
LLM_PROVIDER | Which language model to use. Only deepseek or grok are supported at the moment. The template defaults to deepseek. |
DEEPSEEK_API_KEY | Your DeepSeek API key. Needed only when LLM_PROVIDER is deepseek. Starts with sk-. |
GROK_API_KEY | Your Grok API key. Needed only when LLM_PROVIDER is grok. Starts with xai-. |
NOVELAI_API_KEY | Your NovelAI key. Starts with pst-. |
APP_USERNAMEAPP_PASSWORD | The username and password for logging in to the Vallotton web page. |
ADMIN_PASSWORD | The password for the admin backend. You will hardly ever need it; just set a long random password. |
DJANGO_SECRET_KEY | A string of random characters, used to protect the login state. |
POSTGRES_PASSWORD | The database password. You will never use it yourself, so again a string of random characters is fine. |
DOMAIN | Your domain name, without the https:// prefix. For example vallotton.example.com. |
MEDIA_DIR | The directory for the pictures. Enter the /srv/vallotton/media you created in the previous step. |
The following items are optional but commonly used:
OPENROUTER_API_KEY: an OpenRouter API key. With it set, if the language model's official endpoint cannot be reached, Vallotton switches temporarily to the OpenRouter backup route by itself.MCP_TOKEN: the MCP key. Needed only if you intend to enable MCP. Usually a string of random characters; you copy it when you need it.- The model names and reasoning effort for each provider, such as
DEEPSEEK_MODELandGROK_REASONING_EFFORT: change these if you want a different model or want things faster or slower. The full list is in the environment variables appendix below.
The rest can usually stay at their defaults. The complete list is in the environment variables appendix at the end.
3. Start
In the code directory, run:
docker compose up -d --build
The first start downloads and builds a few Docker images, which takes a few minutes. When it is done, check with these two commands:
docker compose ps
curl -s http://127.0.0.1:8300/api/auth/me
The first should list three containers, db, backend and web, all with status Up. The second should return {"authenticated": false}, which means Vallotton is running on port 8300 of the server itself.
It cannot be reached from the internet yet, because port 8300 is open only to the server itself. Access from outside goes through nginx, which is configured in the next step. If port 8300 is already taken by another program on the server, change VALLOTTON_HTTP_PORT in .env to another port and run docker compose up -d again; the nginx configuration in the next step then has to change to match.
4. Domain and HTTPS
The code ships with an nginx configuration template, deploy/host-nginx.conf.example, which forwards visits to your domain on to Vallotton. The commands below configure nginx automatically. In these commands, and in the rest of the chapter, the example domain is vallotton.example.com. Before running them, replace vallotton.example.com with your own domain.
sudo sh -c 'sed "s/YOUR_DOMAIN/vallotton.example.com/g" deploy/host-nginx.conf.example > /etc/nginx/sites-available/vallotton'
sudo ln -s /etc/nginx/sites-available/vallotton /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Then get an HTTPS certificate with certbot (again, replace the example domain vallotton.example.com with your own). It edits the configuration you just created to add HTTPS, and makes http visits redirect to https automatically:
sudo certbot --nginx -d vallotton.example.com
The certificate is valid for 90 days. certbot renews it by itself before it expires, so nothing more is needed.
If your domain is on Cloudflare: before requesting the certificate with certbot, switch that record to “DNS only” (the grey cloud) in Cloudflare, and switch it back to proxied once the certificate has been issued.
You can also skip certbot and use a Cloudflare Origin Certificate instead: generate one in Cloudflare under SSL/TLS → Origin Server. It is valid for 15 years and never needs renewing. Put the certificate and the private key on the server, and add a server block for port 443 to the nginx configuration that points at them. A certificate of this kind only works for visits that go through Cloudflare's proxy.
Whichever certificate you use, Cloudflare's SSL/TLS mode must be “Full (strict)”, not “Flexible”; otherwise the page opens but you cannot log in.
5. Open the page
If all went well, open https://vallotton.example.com (your own domain) in a browser and log in to Vallotton with the APP_USERNAME and APP_PASSWORD you set in .env. From here on, everything works exactly as described in the quick start and the user guide. You can also install Vallotton as an app on your phone; see on a phone in the user guide.
One thing to keep in mind: you must open it through https and the domain name. If you open it over plain http or by the server's IP address, the page may display but you cannot log in. That is deliberate, for security.
Day-to-day maintenance
All the commands below are run in the code directory, /srv/vallotton/app.
- Update to a new version: pull the latest code, then rebuild and restart.
git pull && docker compose up -d --build - Change the configuration: after editing
.env, rundocker compose up -dand the change takes effect. The web login username and password are changed the same way; they are updated automatically at every start. - View the logs: when something goes wrong and you want to see what happened, look at the backend log. Press Ctrl+C to leave it.
docker compose logs -f backend - Stop: run
docker compose down. Conversations and pictures are kept, and the nextdocker compose up -dcarries on from there. Do not add-v; that deletes the database as well. - Back up: all of Vallotton's data is the database plus the pictures directory (pictures are kept in a separate folder rather than inside the database, for performance and easier management). These commands back up both:
To restore the database from a backup (replacedocker compose exec db pg_dump -U vallotton vallotton | gzip > backup-$(date +%F).sql.gz tar czf media-$(date +%F).tar.gz -C /srv/vallotton mediaDATEwith the date suffix of the file):gunzip -c backup-DATE.sql.gz | docker compose exec -T db psql -U vallotton vallotton
Vallotton also has an admin backend that shows every conversation, message and picture, and the API spend per day. For security it is not exposed by default, and you will not normally need it. If you really do, create a docker-compose.override.yml in the code directory that gives backend ports: ["127.0.0.1:8000:8000"], temporarily set COOKIE_SECURE=0 in .env, and restart the service. Then connect to the server through an SSH tunnel, open http://127.0.0.1:8000/admin/ and log in with ADMIN_USERNAME and ADMIN_PASSWORD. Remember to undo both changes when you are done.
MCP
MCP is an interface Vallotton provides for reaching its internal services through a URL. Other programs can use it to send a line of text and get the finished picture back, or to draw directly from a prompt, check the balance, and so on. Conversations started through MCP also show up in the left bar of the web page.
Note that this interface uses Vallotton's own simplified, home-grown variant of the protocol, not standard MCP, so it cannot be connected directly from clients like Claude Desktop or Cursor (MCP is not that common these days anyway...).
To enable it:
- Set
MCP_TOKENin.envto any string you like. This is the key to your Vallotton's MCP service. - Run
docker compose --profile mcp up -d --build. You can check withcurl -s http://127.0.0.1:8301/health; if the response contains"ok": true, it is up. - By default MCP listens only on port 8301 of the server itself. To reach it from the internet, set
MCP_BIND=0.0.0.0in.envand run the previous command again. Remember to open port 8301 in your cloud provider's console.
To use MCP, just give your coding agent the following information, and it will figure out on its own how to talk to Vallotton's MCP service.
{
"name": "vallotton",
"url": "http://SERVER_IP:8301",
"bearer_token": "the MCP_TOKEN from your .env"
}
Two things to keep in mind. First, if you open it to the internet, MCP is reached from outside over plain HTTP. Second, a picture can take several minutes, so set the client's timeout to 600 seconds or more.
About security
If you deploy by this guide, Vallotton already has these protections by default:
- Only nginx's ports 80 and 443 are open to the outside. Vallotton's own port and the database stay inside the server and cannot be reached from outside. MCP is exposed only when you set
MCP_BIND=0.0.0.0. - Eight wrong passwords in a row on the web login lock it for 15 minutes. The web login and the admin backend are two separate accounts.
- Finished pictures can only be seen after logging in; someone who has a picture's address cannot open it.
- Uploaded images are checked: only image formats are accepted, at most 15 MB each.
What you have to take care of yourself: use long random strings for all the passwords in .env; never share the .env file or commit it to git; and in your cloud provider's console open only 22, 80 and 443, plus the MCP port if you really do mean to expose it.
Appendix: if something goes wrong
A short checklist.
| What you see | Cause and what to do |
|---|---|
| The page opens, but you cannot log in | Check that you opened it as https://YOUR_DOMAIN; opening it by IP address or over http will not let you log in. If you use Cloudflare, check that the SSL/TLS mode is “Full (strict)”. A server clock that is far off causes this too. |
| Logging in says you are locked out | Eight wrong passwords in a row lock the login for 15 minutes. Wait a while and try again. |
| Starting says the port is in use | Change VALLOTTON_HTTP_PORT in .env (or MCP_PUBLIC_PORT for MCP) to another port, run docker compose up -d, then change the port in the proxy_pass line of the nginx configuration to match and reload nginx. |
| A picture stops halfway with “The task was interrupted” | Vallotton was restarted while painting, for example right as you updated to a new version. Send the message again. |
| It stays at “is thinking it over” for ever | The language model's official endpoint cannot be reached. You can set up the backup route with OPENROUTER_API_KEY in .env; when the official endpoint is down, Vallotton switches to the backup route by itself. “Routes” on the Settings page shows which one is in use at the moment. |
| Painting fails and the error mentions “402” | The NovelAI account's points (Anlas) are used up, usually because the quality was set to “High”. Set it back to “Mid” in the generation settings. |
| NovelAI shows “status unknown” | Now and then NovelAI's account status cannot be fetched. It does not affect painting and recovers by itself in a few minutes. |
| The MCP client times out | Set the client's timeout to 600 seconds or more. A drawing request only returns once the picture is finished. |
Appendix: environment variables
Below are the items that can be set in .env, grouped by purpose. “Default” is the value used when the item is left out. Items marked “required” have no default and must be given a value; without one, Vallotton will not start, or the feature in question will not work.
The example values in .env.example may differ from the defaults; what counts is what is actually written in .env. For a deployment you normally only need to change the items listed in step 2.
| Variable | Default | Description |
|---|---|---|
| Language model | ||
LLM_PROVIDER | deepseek | Whose language model to use: deepseek or grok. |
DEEPSEEK_API_KEY | — | DeepSeek key. Required when using DeepSeek. |
DEEPSEEK_MODEL | deepseek-flash | Which DeepSeek model to use. |
DEEPSEEK_REASONING_EFFORT | high | DeepSeek's reasoning effort when understanding the conversation and planning the picture: low, high or max. max more than doubles the wait for every picture without making it better. |
DEEPSEEK_DRAW_REASONING_EFFORT | high | DeepSeek's reasoning effort when writing the drawing prompt; same values. |
DEEPSEEK_MAX_TOKENS | 128000 | The most tokens one answer may contain. Set it too small and answers get cut off easily. |
DEEPSEEK_RETRIES | 2 | How many times to retry automatically when DeepSeek's answer is empty or malformed (DeepSeek has formatting hiccups now and then). |
DEEPSEEK_CONTEXT_LIMIT | 1000000 | DeepSeek's context limit. |
DEEPSEEK_LONG_CONTEXT_THRESHOLD | 400000 | The conversation context length at which you get a reminder, once, when using DeepSeek. |
OPENROUTER_API_KEY | empty | OpenRouter key. DeepSeek's official service is not always stable; with an OpenRouter key set, a timeout on the official endpoint switches automatically to the same model on OpenRouter. |
OPENROUTER_BASE | https://openrouter.ai/api/v1 | Address of the OpenRouter backup route. |
OPENROUTER_MODEL | deepseek/deepseek-v4.1-flash | The model used on the OpenRouter backup route. |
LLM_ROAD_BLOCKED_SECONDS | 1800 | After the official endpoint fails, how many seconds at most to route around it. |
LLM_ROAD_PROBE_SECONDS | 60 | While routing around the official endpoint, how many seconds between attempts to use it again. |
GROK_API_KEY | — | xAI key. Required when using Grok. |
GROK_MODEL | grok-4.6 | Which Grok model to use. |
GROK_REASONING_EFFORT | high | Grok's reasoning effort when understanding the conversation and planning the picture. high corresponds to what xAI calls the expert level. |
GROK_DRAW_REASONING_EFFORT | medium | Grok's reasoning effort when writing the drawing prompt. |
GROK_TITLE_MODEL | grok-4.20-non-reasoning | The model Grok uses to give conversations their titles automatically. Leave it empty to use GROK_MODEL.(Incidentally, with DeepSeek the titles are written by DEEPSEEK_MODEL directly and there is no separate setting, because DeepSeek is cheap anyway.) |
GROK_CONTEXT_LIMIT | 500000 | Grok's context limit. |
GROK_LONG_CONTEXT_THRESHOLD | 200000 | The conversation context length at which you get a reminder, once, when using Grok. |
GROK_MAX_TOKENS | 20000 | The most tokens one Grok answer may contain. |
GROK_CONTEXT_WARN_RATIO | 0.8 | At what fraction of the context the warnings begin. This one applies whichever provider you use. |
LLM_TIMEOUT_SECONDS | 420 | How many seconds without any output from the model count as a timeout. |
LLM_ANSWER_SECONDS | 600 | How many seconds at most to wait for one answer; beyond that the question is asked again. |
LEAD_TOOL_ROUNDS | 2 | How many times at most the tag table is looked up while understanding the conversation and planning the picture. 0 means no lookups. |
DRAW_TOOL_ROUNDS | 4 | How many times at most the tag table is looked up while writing the picture into a prompt. 0 means no lookups. |
LEAD_USAGE_SCENARIODRAW_USAGE_SCENARIO | empty | The “usage scenario” passage at the start of the system prompt. |
DRAW_TEMPERATURE | 0 | The randomness when writing the picture into a prompt. This step is just translation and needs no creativity, so the default is 0. |
| Drawing | ||
NOVELAI_API_KEY | — | NovelAI key, starting with pst-. |
NAI_MODEL | nai-diffusion-5-full | The NovelAI model used at Mid and High quality (Low quality always uses the previous generation, V4.5). |
| Reflection and continuous generation | ||
MAX_REFLECTION_ROUNDS | 10 | The highest value “Reflection rounds” can be set to. |
MAX_CONTINUOUS_ROUNDS | 20 | The highest value “Continuous” can be set to. |
FREE_RETRIES_IN_RUN | 2 | How many retries when reflection or continuous generation fails midway. |
JOB_STALE_SECONDS | 180 | How many seconds a task may go without progress before it counts as interrupted. |
JOB_SWEEP_SECONDS | 60 | How many seconds between checks for interrupted tasks; the same check also sends the next line of a continuous generation. 0 means no checks (and then continuous generation does not continue either). |
| Accounts | ||
APP_USERNAME | user | Username for the web login. |
APP_PASSWORD | — | Password for the web login. |
ADMIN_USERNAME | admin | Username for the admin backend. |
ADMIN_PASSWORD | — | Password for the admin backend. |
| Site | ||
DJANGO_SECRET_KEY | an insecure placeholder | The key that protects the login state. Change it to a string of random characters when deploying. It starts without the change, but is not secure. |
DOMAIN | empty | Your domain name. Logging in is only possible through this domain. |
DJANGO_DEBUG | 0 | Debug switch: 1 is on, 0 is off. Keep it at 0 when deploying. If you deploy by this guide, compose already pins it to 0, and changing it has no effect. |
COOKIE_SECURE | 1 | At 1, the login state travels only over https. Set it to 0 only while the admin backend is temporarily enabled, and change it back afterwards. |
TRUSTED_PROXY_HOPS | 1 | How many layers of reverse proxy sit in front of Vallotton on the server. Deployed by this guide, it is 1 and normally needs no change. Getting the count wrong disables the login attempt limit. |
| Database and pictures | ||
POSTGRES_PASSWORD | — | Database password; a string of random characters is fine. |
POSTGRES_DBPOSTGRES_USER | vallotton | Database name and username; normally no need to change. |
POSTGRES_HOSTPOSTGRES_PORT | 127.0.0.15432 | Database address and port. Deployed by this guide, compose always connects to its own database container, so no change is needed. Only used when running the backend outside docker. |
MEDIA_DIR | ./tmp/media | The directory on the server where pictures are kept. Use an absolute path when deploying. |
| Ports and MCP | ||
VALLOTTON_HTTP_PORT | 8300 | The port Vallotton opens on the server itself; nginx forwards visits here. No need to change it unless it clashes with another application's port. |
MCP_PUBLIC_PORT | 8301 | MCP's port on the server. Only used when MCP is enabled. |
MCP_BIND | 127.0.0.1 | Who the MCP port is open to. 127.0.0.1 means only the server itself; 0.0.0.0 means it can be reached directly from outside. |
MCP_TOKEN | — | The key that MCP requests must carry. Required when MCP is enabled. |
MCP_HOSTMCP_PORT | 127.0.0.18766 | The listen address and port when running mcp/server.py on its own outside docker. No change needed with compose. |
VALLOTTON_BACKEND_URL | http://127.0.0.1:8000 | Which backend MCP forwards requests to. With compose it points at the backend container automatically; no change needed. |
A few variables used only in development and testing (such as switching the database to sqlite, or painting placeholder images instead of calling NovelAI) are not listed here.
