Deploy Readiness Audit
The Pre-Flight Checklist
Section titled “The Pre-Flight Checklist”At this point, we have verified:
- MongoDB running in Docker
- the Express API talking to MongoDB
- Passport-based admin authentication
- Vite serving the frontend locally
- public submission and admin moderation working together end to end
it works locally as a complete system.
Good. That confidence matters.
Now we can safely do the next job: remove the local assumptions that would break the moment this app leaves Docker Compose and lands on a hosted platform.
This is the refactor that makes deployment possible.
Our readiness audit has five checkpoints:
- Dynamic Port Binding — let the host choose the port
- Dynamic Database Routing — move the Mongo connection string into an environment variable
- Dynamic Session Secret — stop treating auth secrets like classroom decorations
- Production Static File Serving — teach Express to serve the compiled Vite app
- Preserve the Local Bridge — keep Vite proxying in development where it belongs
1. Relinquishing the Port
Section titled “1. Relinquishing the Port”A hosted platform does not care that we enjoy port 3000.
It provides a port dynamically, usually through process.env.PORT.
const PORT = process.env.PORT || 3000;const HOST = process.env.HOST || '0.0.0.0';Now our server will behave correctly in both environments:
- locally it falls back to
3000 - in production it accepts the host-provided port
A hosted environment also expects the server to listen on the container’s network interface, not just on localhost.
Binding to 0.0.0.0 tells Express to listen on on all available network interfaces, which allows traffic coming into the container to reach the app.
This works perfectly well in our locally running Docker containers and becomes essential later in our hosted deployment.
Let’s make our Express app listen on all network interfaces, not just localhost:
app.listen(PORT, HOST, () => { console.log(`Voyager's Log API listening on ${HOST}:${PORT}`);});2. Dynamic Database Routing
Section titled “2. Dynamic Database Routing”When testing we may want to continue using our local MongoDB, but for production we’re going to point to Atlas.
We’ll need our DB connection string to be environment specific for that:
const MONGO_URI = process.env.MONGO_URI;Then continue using it in the Mongoose connection:
mongoose .connect(MONGO_URI) .then(() => { console.log('Connected to MongoDB'); seedAdminUser(); }) .catch((error) => { console.error('MongoDB connection error:', error); });Now the code no longer cares where MongoDB lives.
It only cares that a valid connection string is provided at runtime.
That is exactly the kind of indifference we want.
When we move to MongoDB Atlas, the connection string will include real credentials. That value must come from the environment, not from source control and definitely not from a committed JavaScript string.
3. Dynamic Session Secret
Section titled “3. Dynamic Session Secret”Our auth layer also needs to stop assuming a classroom-only secret.
replace it with:
const SESSION_SECRET = process.env.SESSION_SECRET || 'dev-only-secret-change-me';That gives us a reasonable local fallback while preparing the app for a real production secret later.
Use that variable in your session setup:
app.use( session({ secret: SESSION_SECRET, resave: false, saveUninitialized: false, store: MongoStore.create({ mongoUrl: MONGO_URI, }), cookie: { httpOnly: true, maxAge: 1000 * 60 * 60 * 24, }, }));This is a nice example of environment awareness reaching beyond just ports and databases.
Secrets are configuration too.
4. Serving the Compiled Frontend
Section titled “4. Serving the Compiled Frontend”Locally, Vite is a dev server.
In production, Vite becomes a build tool.
That means it generates static frontend assets into:
client/distand then gets out of the way.
If we want the app to run cleanly from one origin in production, Express needs to serve those built files directly.
Import path
Section titled “Import path”At the top of server/index.js, add:
const path = require('path');Add a Production-Only Static Serving Block
Section titled “Add a Production-Only Static Serving Block”After your API routes, add:
if (process.env.NODE_ENV === 'production') { const distPath = path.join(__dirname, '../client/dist');
app.use(express.static(distPath));
app.get('/{*splat}', (req, res) => { res.sendFile(path.join(distPath, 'index.html')); });}This does two jobs:
Static Assets
Section titled “Static Assets”app.use(express.static(distPath));This serves the built CSS, JavaScript, and other static frontend assets.
Catch-All Frontend Delivery
Section titled “Catch-All Frontend Delivery”app.get('/{*splat}', (req, res) => { res.sendFile(path.join(distPath, 'index.html'));});This ensures that browser requests for the frontend still load the app properly instead of returning 404 errors.
So in production:
/api/...requests hit the Express API routes- frontend requests get the built
index.htmland its assets
That gives us one app, one origin, and far less deployment drama.
We wrap this block in if (process.env.NODE_ENV === 'production') because we
still want Vite to handle the frontend during local development. Fast dev
feedback locally, integrated static serving later. Everybody wins.
Once Express is responsible for serving the frontend in production, the Vite build step becomes mandatory. This is why the next phase tightens the Docker story around a build stage and a runtime stage.
The production static-serving block assumes that the built frontend actually exists.
Specifically, it assumes this file is present:
client/dist/index.htmlIf that file does not exist yet, Express has nothing useful to serve.
That means our future deployment pipeline must eventually do the following in the correct order:
- install dependencies
- build the Vite frontend
- start the Express server
We’ll handle exactly this on the next page.
5. Preserving the Local Bridge
Section titled “5. Preserving the Local Bridge”None of this means local development stops using Vite.
Quite the opposite.
Locally, we still want:
- Express on
3000 - Vite on
5173
and we still want Vite to proxy API requests to Express.
Setting the Base Path
Section titled “Setting the Base Path”Add base: '/' to vite.config.js
This helps Vite generate production asset paths correctly from the domain root.
Our client/vite.config.js should now look like:
import { defineConfig } from 'vite';import tailwindcss from '@tailwindcss/vite';
export default defineConfig({ base: '/', plugins: [tailwindcss()], server: { host: '0.0.0.0', port: 5173, proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true, }, }, },});Hosted Deployment Later
Section titled “Hosted Deployment Later”By the end of this lesson, the app is ready to consume values like these.
PORT=<provided by platform>MONGO_URI=<Atlas connection string>SESSION_SECRET=<strong production secret>NODE_ENV=productionThe beauty here is that the code no longer needs to change between environments.
Only the values do.
Extra Bits & Bytes
Section titled “Extra Bits & Bytes”Vite Docs: Static Deployment
⏭ The Multi-Stage Dockerfile
Section titled “⏭ The Multi-Stage Dockerfile”Our application is now runtime-aware, but our container story still needs tightening. Next, we package the build step and the runtime step into a single multi-stage Dockerfile so the compiled Vite frontend and the Express server can ship together cleanly.