In this blog, we’ll walk through the process of integrating WebSocket functionality using Socket.IO, adding caching support with Redis, and hosting a full-stack application built with Flask and Next.js using Docker Compose. By the end, you’ll have a scalable, real-time web application architecture ready to deploy.
Tech Stack Overview
- Backend: Flask with extensions like Flask-SocketIO for real-time communication, Flask-Session for session management, and SQLAlchemy for database interactions.
- Frontend: Next.js with client-side Socket.IO for real-time content synchronization.
- Database: PostgreSQL for persistent data storage.
- Cache: Redis for caching and quick data access.
- Hosting: Docker Compose for containerized deployment.
Step 1: Backend Setup with Flask and Redis
Flask Backend Structure
Here’s the structure of our Flask backend:
- Initialize Flask and Extensions:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_socketio import SocketIO
from flask_session import Session
import redis, os
app = Flask(__name__)
app.config["SECRET_KEY"] = "your_secret_key"
app.config["SQLALCHEMY_DATABASE_URI"] = "postgresql://username:password@postgres:5432/dbname"
app.config["SESSION_TYPE"] = "redis"
app.config["SESSION_PERMANENT"] = False
app.config["SESSION_USE_SIGNER"] = True
app.config["SESSION_KEY_PREFIX"] = "session:"
app.config["SESSION_REDIS"] = redis.StrictRedis(host=os.getenv('REDIS_HOST', 'redis'), port=6379, db=0)
# Initialize extensions
socketio = SocketIO(app, cors_allowed_origins="*")
db = SQLAlchemy(app)
Session(app)
- Add Real-Time WebSocket Functionality:
from flask_socketio import emit, join_room, leave_room
@socketio.on('join_room')
def handle_join_room(data):
room = data.get('contentId')
join_room(room)
emit('user_joined', {'msg': 'A new user has joined the room'}, to=room)
@socketio.on('edit_content')
def handle_edit_content(data):
content_id = data.get('contentId')
content = data.get('content')
emit('content_update', {'content': content}, to=content_id, skip_sid=request.sid)
@socketio.on('leave_room')
def handle_leave_room(data):
room = data.get('contentId')
leave_room(room)
emit('user_left', {'msg': 'A user has left the room'}, to=room)
- Session and Redis Configuration:
import redis
cache = redis.StrictRedis(host=os.getenv('REDIS_HOST', 'redis'), port=6379, db=0)
- Environment Configuration (.env file):
SECRET_KEY=your_secret_key
SQLALCHEMY_DATABASE_URI=postgresql://postgres:postgres@postgres:5432/postgres
SQLALCHEMY_TRACK_MODIFICATIONS=False
APP_URL=AWS EC2 IP address
HOST=redis
Step 2: Frontend Setup with Next.js and Socket.IO
Client-Side Socket.IO Setup
- Install Socket.IO Client:
npm install socket.io-client
- Create a Socket Instance:
// socket.js
import { io } from "socket.io-client";
const SOCKET_URL = process.env.NEXT_PUBLIC_SOCKET_URL;
const socket = io(SOCKET_URL, {
autoConnect: false,
});
export default socket;
- Integrate TipTap Editor with Real-Time Updates:
"use client";
import socket from "@/shared/socket";
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import { useEffect, useState } from "react";
const Tiptap = ({ content = "", contentId }) => {
const [isSaving, setIsSaving] = useState(false);
const editor = useEditor({
extensions: [StarterKit],
content,
onUpdate: ({ editor }) => {
const updatedContent = editor.getHTML();
if (contentId) {
setIsSaving(true);
socket.emit("edit_content", { contentId, content: updatedContent });
setTimeout(() => setIsSaving(false), 1000);
}
},
});
useEffect(() => {
if (contentId) {
if (!socket.connected) socket.connect();
socket.emit("join_room", { contentId });
socket.on("content_update", (data) => {
if (editor && data.content !== editor.getHTML()) {
editor.commands.setContent(data.content);
}
});
return () => {
socket.emit("leave_room", { contentId });
socket.off("content_update");
};
}
}, [contentId, editor]);
return <EditorContent editor={editor} />;
};
export default Tiptap;
- Environment Configuration (.env file):
NEXT_PUBLIC_API_URL=http://localhost:5000/api
NEXT_PUBLIC_SOCKET_URL=http://localhost:5000
DATABASE_NAME=postgres
DATABASE_USER=postgres
DATABASE_PASSWORD=postgres
Step 3: Docker Compose for Deployment
-
\
** Configuration**:
version: "3.8"
services:
nginx:
image: nginx:alpine
container_name: nginx_proxy
ports:
- "443:443"
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
depends_on:
- backend
- website
networks:
- app-network
website:
build:
context: ./website
dockerfile: Dockerfile
args:
NEXT_PUBLIC_SOCKET_URL: ${NEXT_PUBLIC_SOCKET_URL}
container_name: website
environment:
- NEXT_PUBLIC_SOCKET_URL=${NEXT_PUBLIC_SOCKET_URL}
expose:
- "3000"
networks:
- app-network
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: backend
expose:
- "5000"
networks:
- app-network
redis:
image: redis:alpine
container_name: redis
expose:
- "6379"
networks:
- app-network
postgres:
image: postgres:14-alpine
container_name: postgres
environment:
- POSTGRES_DB=${DATABASE_NAME}
- POSTGRES_USER=${DATABASE_USER}
- POSTGRES_PASSWORD=${DATABASE_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
postgres_data:
- Nginx Configuration for Proxy:
server {
listen 80;
server_name _;
location / {
proxy_pass http://website:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /api/ {
proxy_pass http://backend:5000;
proxy_set_header Host $host;
}
location /socket.io/ {
proxy_pass http://backend:5000/socket.io/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Step 4: Running the Application
- Build and start the services:
docker-compose up --build
- Access the application:
-
Frontend:
http://localhost
-
Backend API:
http://localhost/api
-
Frontend:
Top comments (0)